// 
//  SANELaunch.cs
//  
//  Author:
//       Christopher Sawyer <christoofar@gmail.com>
//  
// 
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Lesser General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
// 
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Lesser General Public License for more details.
// 
//  You should have received a copy of the GNU Lesser General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Threading;

namespace Mono.SANE
{
	/// <summary>
	/// Launches the scanimage and scanadf processes
	/// </summary>
	internal class SANELaunch
	{
		
		private const string PROCNAME = "scanimage";
		private const string ADFPROCNAME = "scanadf";
		
		private SANELaunch ()
		{
		}
		
		static Process GetProc (string command)
		{
			Process proc = new Process ();
			ProcessStartInfo si = new ProcessStartInfo (PROCNAME);
			si.UseShellExecute = false;
			si.CreateNoWindow = true;
			si.RedirectStandardOutput = true;
			si.RedirectStandardError = true;
			si.Arguments = command;
			proc.EnableRaisingEvents = true;			
			proc.StartInfo = si;

			return proc;
		}
		
		static Process GetADFProc (string command)
		{
			Process proc = new Process ();
			ProcessStartInfo si = new ProcessStartInfo (ADFPROCNAME);
			si.UseShellExecute = false;
			si.CreateNoWindow = true;
			si.RedirectStandardOutput = true;
			si.RedirectStandardError = true;			
			si.Arguments = command;
			proc.StartInfo = si;
			
			return proc;
		}
		
		/// <summary>
		/// Gets the backend version of SANE
		/// </summary>
		/// <returns>
		/// A <see cref="System.String"/>
		/// </returns>
		public static string GetSANEVersion ()
		{
			Process sane = GetProc ("--version");
			sane.Start ();
			
			string data = sane.StandardOutput.ReadToEnd ();
			
			sane.WaitForExit ();
			sane.Dispose ();
			
			string[] verinfo = data.Split (' ');
			
			return verinfo[verinfo.Length - 1].Replace ("\n", "");
		}
		
		/// <summary>
		/// Gets the version of scanadf
		/// </summary>
		/// <returns>
		/// A <see cref="System.String"/>
		/// </returns>
		public static string GetADFVersion ()
		{
			Process sane = GetADFProc ("--version");
			sane.Start ();
			
			string data = sane.StandardOutput.ReadToEnd ();
			
			sane.WaitForExit ();
			sane.Dispose ();
			
			string[] verinfo = data.Split (' ');
			
			return verinfo[verinfo.Length - 1].Replace ("\n", "");
		}
		
		/// <summary>
		/// Pulls a list of valid imaging devices from SANE
		/// </summary>
		/// <returns>
		/// A <see cref="System.String"/>
		/// </returns>
		public static string[] GetScannerList ()
		{
			Process sane = GetProc ("-f %i^%d^%v^%m^%t%n");
			sane.Start ();
			
			string[] data = sane.StandardOutput.ReadToEnd ().Split ('\n');
			
			sane.WaitForExit ();
			sane.Dispose ();
			
			List<string> scanlines = new List<string> ();
			
			//Filter out USB error chaff
			foreach (string info in data)
			{
				if (info.Contains ("^"))
				{
					scanlines.Add (info);
				}
			}
			
			return scanlines.ToArray ();
		}

		/// <summary>
		/// Asynchronous threaded code to kick off a background scan job.
		/// </summary>
		/// <param name="scanObj">
		/// A <see cref="System.Object"/> representing the Scanner to kick off scanning
		/// </param>
		private static void BeginMultiPageScan (object scanObj)
		{
			Scanner scanner = (Scanner)scanObj;
			
			string options = "-p ";
			
			//Add scanner options
			foreach (ScannerOption option in scanner.Options.Values) {
				if (option.CurrentSetting != string.Empty)
				{
					if (option.CurrentSetting.Contains (" "))
					{
						options += "--" + option.Switch + @" """ + option.CurrentSetting + @""" ";					
					} 
					else 
					{
						options += "--" + option.Switch + " " + option.CurrentSetting + " ";
					}
				}
			}
			
			//Add batch options
			options += @"--batch=/tmp/imgproc%d.tif --format=tiff";

			Process sane = GetProc (@"--device-name """ + scanner.Deviceid + @""" " +
				options + @" --format=tiff");
			sane.StartInfo.UseShellExecute = false;
			sane.StartInfo.RedirectStandardOutput = true;
			sane.StartInfo.RedirectStandardError = true;
			
			Encoding currentEncoding = null;
			
			bool done = false;
			bool errReady = true;
			byte[] errBuffer = new byte[4096];
			//StringBuilder progress = new StringBuilder ();
			
			AsyncCallback errCallBack = delegate(IAsyncResult sender)
			{
				int bytesRead = sane.StandardError.BaseStream.EndRead (sender);
				if (bytesRead <= 0) {
					done = true;
					return;
				}
				string s = currentEncoding.GetString (errBuffer, 0, bytesRead);
				scanner.ParseADFMessage (s);				
				//progress.Append (s);
				errReady = true;
			};
			
			sane.OutputDataReceived += delegate(object sender, DataReceivedEventArgs e) {			
				//Console.WriteLine ("STANDARD OUTPUT");
				//Console.WriteLine (e.Data + "//");
			};
			sane.Exited += delegate(object sender, EventArgs e) { Console.WriteLine ("Scanning process shut down."); };


			sane.Start ();
			currentEncoding = sane.StandardError.CurrentEncoding;
		
			//			sane.BeginErrorReadLine ();
			sane.BeginOutputReadLine ();
			while (!done)
			{
				if (errReady)
				{
					errReady = false;
					sane.StandardError.BaseStream.BeginRead (
						errBuffer,
						0,
						errBuffer.Length,
						errCallBack,
						null
						);
				}
				if (!done) 
				{
					Thread.Sleep (100);
				}
			}								
		}
		
		/// <summary>
		/// Begins a multi image scan from a scanner equipped with an Automatic
		/// Document feeder.  Hook the page events to receive information about
		/// scanned pages.
		/// </summary>
		/// <param name="scanner">
		/// A <see cref="Scanner"/>
		/// </param>
		/// <returns>
		/// A <see cref="System.Drawing.Image[]"/>
		/// </returns>
		public static void StartMultiPageScan (Scanner scanner)
		{
			Thread t = new Thread (new ParameterizedThreadStart(BeginMultiPageScan));
			t.Start (scanner);
		}
		
		/// <summary>
		/// Scans a picture directly from the scanner with the options given
		/// </summary>
		/// <param name="scanner">
		/// A <see cref="Scanner"/>
		/// </param>
		/// <returns>
		/// A <see cref="System.Byte[]"/>
		/// </returns>
		public static System.Drawing.Image RawSinglePageScan (Scanner scanner)
		{
			string options = "";
			
			foreach (ScannerOption option in scanner.Options.Values)
			{
				if (option.CurrentSetting != string.Empty)
				{
					if (option.CurrentSetting.Contains (" "))
					{
						options += "--" + option.Switch + @" """ + option.CurrentSetting + @""" ";					
					} 
					else 
					{
						options += "--" + option.Switch + " " + option.CurrentSetting + " ";
					}
				}
			}
			
			Process sane = GetProc (@"--device-name """ + scanner.Deviceid + @""" " + options + @" --format=tiff");
			sane.StartInfo.UseShellExecute = false;
			sane.StartInfo.RedirectStandardOutput = true;
			sane.StartInfo.RedirectStandardError = true;
			MemoryStream mem = new MemoryStream ();
			BinaryWriter w = new BinaryWriter (mem);
			
			sane.Start ();

			StreamCopy (sane.StandardOutput.BaseStream, w.BaseStream);
			
			sane.WaitForExit ();
			
			if (sane.StandardError.ReadToEnd ().ToLower ().Contains ("document feeder out of documents")) 
			{
				scanner.Started = false;
				sane.Dispose ();
				w.Close ();
				mem.Close ();
				mem.Dispose ();
				throw new NoDocumentLoadedException ("No document found in the document feeder.");
			} 
			else 
			{
				scanner.Started = false;
				sane.Dispose ();
				System.Drawing.Image image = System.Drawing.Image.FromStream (mem);
				
				w.Close ();
				mem.Close ();
				mem.Dispose ();
				
				return image;
			}
		}		

   		public static Image Crop (Image p)
   		{
   			Int32 x = default(Int32);
   			Int32 y = default(Int32);

			Bitmap b = new Bitmap (p);
   			Color c = new Color ();
   
			Int32 intLeft = default(Int32);
   			Int32 intRight = default(Int32);
   			Int32 intBottom = default(Int32);
   			Int32 intTop = default(Int32);

			y = 0;
   			while (!(y == b.Height)) {
   				//loop through pixels on Y axis until end of image height
   				x = 0;
   				while (!(x == b.Width)) {
   					//loop through pixels on X axis until end of image width
   					c = b.GetPixel (x, y);
   					//Get the color of the pixel
   					//First we want to establish if the pixel is White (the color of our background)
   					if (c.R != 0 & c.G != 0 & c.B != 0) {
   						//We'll also want to ignore colors close to the background color
   						//I did this because some of the images I had were scanned, and 
   						//the paper color didn't always scan at a perfect white
   						if (c.R < 250 | c.G < 250 | c.B < 250) {
   							//Determine if pixel is further left than the value we already have
   							if (intLeft <= 0 & intLeft < x) {
   								intLeft = x;
   							}
   							//Determine if pixel is further to the top than the value we already have
   							if (intTop <= 0 & intTop < y) {
   								intTop = y;
   							}
   							//Determine if pixel is further right than the value we already have
   							if (intRight <= b.Width & intRight < x) {
   								intRight = x;
   							}
   							//Determine if pixel is further to the bottom than the value we already have
   							if (intBottom <= b.Height & intBottom < y) {
   								intBottom = y;
   							}
   						}
   					}
   					x += 1;
   				}
   				y += 1;
   			}
   
   			Int32 intNewWidth = (intRight);
   			//Establish width of new cropped image
   			Int32 intNewHeight = (intBottom);
   			//Establish height of new cropped image			
   
			//Create new image with a 10 pixel buffer, so image won't be too close to edge
   			Bitmap imgCropped = new Bitmap (intNewWidth + 10, intNewHeight + 10);
   			//Create a Graphics object for drawing to the cropped image
   			Graphics objGraphics = Graphics.FromImage (imgCropped);
   			//set the background color to white (you can choose what you like
   			objGraphics.Clear (System.Drawing.Color.White);
			
			//Determine where you'd like to start drawing from the Top (Y axis)
			Int32 intStartTop = 5;

			//Determine where you'd like to start drawing from the Left (X axis)
			Int32 intStartLeft = 5;


			//Draw the original image to your new cropped sized image
			objGraphics.DrawImage (b, intStartLeft, intStartTop);
			b.Dispose ();
			objGraphics.Dispose ();

			return imgCropped;
		}
				
		public static void StreamCopy (Stream input, Stream output)
		{
			int i;
			byte b;
			
			i = input.ReadByte ();
			
			while (i != -1) {
				b = (byte)i;
				output.WriteByte (b);
				
				i = input.ReadByte ();
			}
		}

		
		public static Gdk.Pixbuf ImageToPixbuf (System.Drawing.Image image)
		{
			using (MemoryStream stream = new MemoryStream ()) {
				image.Save (stream, ImageFormat.Bmp);
				stream.Position = 0;
				Gdk.Pixbuf pixbuf = new Gdk.Pixbuf (stream);
				return pixbuf;
			}
		}
		
		public static string GetTemporaryFile ()
		{
			return Path.GetTempFileName () + ".tif";
		}
		
		public static void DeleteTemporaryFile (string filename)
		{
			File.Delete (filename);
		}
		
		/// <summary>
		/// Reads a SANE scanner device ID and probes for common features that the device should support
		/// </summary>
		/// <returns>
		/// A list of <see cref="ScannerOption"/>
		/// </returns>
		public static List<ScannerOption> GetScannerOptions (string device)
		{
			List<ScannerOption> options = new List<ScannerOption> ();
			
			Process sane = GetProc ("--help -d \"" + device + "\"");
			sane.Start ();
			
			string[] data = sane.StandardOutput.ReadToEnd ().Split ('\n');
			List<string> dataList = new List<string> ();
		
			sane.WaitForExit ();
			sane.Dispose ();
			
			List<string> optionLines = new List<string> ();
			
			//Find the index in the data where the option information is
			int optionsIndex = 0;
			for (int i = 0; i < data.Length; i++)
			{
				if (data[i].Contains ("Options specific to device")) 
				{
					optionsIndex = i;
					break;
				}
			}
			
			dataList.AddRange (data);
			dataList.RemoveRange (0, optionsIndex);
			
			//Filter out the noise and get to the major options
			foreach (string info in dataList) 
			{
				if (info.Contains ("--")) {
					if (info.Contains ("list of all options") || info.Equals (null)) 
					{
						break;
					}
					
					optionLines.Add (info.Trim ());
				}
			}
			
			//Read each option line and turn this into a configurable Option
			foreach (string line in optionLines)
			{
				//Switchable option
				if (!line.Contains ("..") && (!line.Contains ("="))) 
				{
					//string[] config = line.Split (' ');
					string configline = line.Replace (" [", "^[");
					
					if (configline.IndexOf (' ') < 0)
					{
						// that a config with no options available
						// for example --eject^[inactive]
						// discard
						continue;
					}
					
					string optionName = configline.Substring (0, configline.IndexOf (' ')) + '^';
					string rest = configline.Substring (optionName.Length, configline.Length -
						optionName.Length);
					
					configline = optionName + rest;
					
					string[] config = configline.Split ('^');
					
					if (config.Length != 3)
					{
						//We don't know what this line is.  Discard
						continue;
					}
					
					//Strip units of measure
					string[] availableOptions = config[1].Split ('|', '.');
					for (int x = 0; x < availableOptions.Length; x++) 
					{
						availableOptions[x] = availableOptions[x].Replace ("dpi", "");
						availableOptions[x] = availableOptions[x].Replace ("inches", "");
						availableOptions[x] = availableOptions[x].Replace ("px", "");
						availableOptions[x] = availableOptions[x].Replace ("pixels", "");
						availableOptions[x] = availableOptions[x].Replace ("pt", "");
					}
					
					//Add option
					ScannerOption option = new ScannerOption (config[0].Replace ("--", ""),
						availableOptions, config[2].Replace ("[", "").Replace ("]", ""));
					
					options.Add (option);
				}
			}
			
			// If resolution wasn't detected, then add it and set it to 200
			bool found = false;
			foreach (ScannerOption option in options)
			{
				if (option.Switch.ToLower () == "resolution") 
				{
					found = true;
				}
			}
			if (!found)
			{
				options.Add (new ScannerOption ("resolution", 
					new string[] { "100", "200", "300", "600", "1200" }, "200"));
			}
			
			return options;
		}		
		
	}
}